package ch.prokopovi.ui.main;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import pl.mg6.android.maps.extensions.ClusteringSettings;
import pl.mg6.android.maps.extensions.GoogleMap;
import pl.mg6.android.maps.extensions.GoogleMap.InfoWindowAdapter;
import pl.mg6.android.maps.extensions.GoogleMap.OnCameraChangeListener;
import pl.mg6.android.maps.extensions.GoogleMap.OnMarkerClickListener;
import pl.mg6.android.maps.extensions.Marker;
import pl.mg6.android.maps.extensions.SupportMapFragment;
import android.app.Activity;
import android.app.AlertDialog;
import android.location.Location;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
import ch.prokopovi.R;
import ch.prokopovi.api.struct.BestRatePlace;
import ch.prokopovi.struct.Master.CurrencyCode;
import ch.prokopovi.struct.Master.OperationType;
import ch.prokopovi.struct.Master.Region;
import ch.prokopovi.ui.main.api.OpenListener;
import ch.prokopovi.ui.main.api.Opener;
import ch.prokopovi.ui.main.api.UpdateListener;
import ch.prokopovi.ui.main.api.Updater;

import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.LatLngBounds.Builder;
import com.google.android.gms.maps.model.MarkerOptions;

/**
 * near places map fragment
 * 
 * for separate impl. of sherlock map activity see
 * http://stackoverflow.com/questions
 * /13721929/using-actionbarsherlock-with-the-new-supportmapfragment
 * 
 * @author Pavel_Letsiaha
 * 
 */
public class NearFragment extends SupportMapFragment implements UpdateListener,
		OpenListener {

	private static final int DEFAULT_ZOOM = 12;

	private static final String LOG_TAG = "NearFragment";

	private static final float ZOOM_CLASTER_THRESHOLD = 18.0f;

	private LatLng currentPosition;

	private GoogleAnalyticsTracker tracker;

	private GoogleMap map;

	private Updater updater;
	private Opener opener;

	private NearInfoWindowAdapter infoWindowAdapter;
	private final NearCameraListener cameraListener = new NearCameraListener();
	private final NearMarkerClickListener markerClickListener = new NearMarkerClickListener();

	private final List<BestRatePlace> places = new ArrayList<BestRatePlace>();

	private boolean moveToMyLocation = true;

	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);

		Log.d(LOG_TAG, "onActivityCreated");

		if (getActivity() == null)
			return;

		this.tracker = this.updater.getTracker();
		this.tracker.trackPageView("/near");

		// We have a menu item to show in action bar.
		// setHasOptionsMenu(true);

		this.map = getExtendedMap();

		if (!checkReady())
			return;

		this.map.setClustering(buildClusteringSettings(true));

		this.map.setMyLocationEnabled(true);

		this.infoWindowAdapter = new NearInfoWindowAdapter(savedInstanceState);
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {

		Log.d(LOG_TAG, "onCreateView");

		View v = super.onCreateView(inflater, container, savedInstanceState);

		// We have a menu item to show in action bar.
		setHasOptionsMenu(true);

		v.setLayoutParams(new LinearLayout.LayoutParams(0,
				LayoutParams.MATCH_PARENT, 1 - TabsActivity.DUAL_PANE_RATIO));

		return v;
	}

	private ClusteringSettings buildClusteringSettings(boolean withClustering) {

		ClusteringSettings settings = new ClusteringSettings().enabled(
				withClustering).addMarkersDynamically(true);

		if (withClustering) {
			settings = settings //
					.clusterOptionsProvider(
							new ClusterDataProvider(getResources())) //
					.clusterSize(96);
		}

		return settings;
	}

	private void listenersOn() {
		Log.d(LOG_TAG, "listenersOn");

		if (!checkReady())
			return;

		this.map.setInfoWindowAdapter(this.infoWindowAdapter);

		this.updater.addUpdateListener(this);
		this.map.setOnCameraChangeListener(this.cameraListener);
		this.map.setOnMarkerClickListener(this.markerClickListener);
	}

	private void listenersOff() {
		Log.d(LOG_TAG, "listenersOff");

		if (!checkReady())
			return;

		this.map.setInfoWindowAdapter(null);

		this.map.setOnMarkerClickListener(null);
		this.map.setOnCameraChangeListener(null);
		this.updater.removeUpdateListener(this);
	}

	@Override
	public void onResume() {

		Log.d(LOG_TAG, "onResume");

		super.onResume();

		if (!checkReady())
			return;

		listenersOn();

		Location myLastLocation = this.updater.getLocation();

		if (this.moveToMyLocation && myLastLocation != null && checkReady()) {

			this.map.moveCamera(CameraUpdateFactory.newLatLngZoom(
					new LatLng(myLastLocation.getLatitude(), myLastLocation
							.getLongitude()), DEFAULT_ZOOM));

			Marker nearestMarker = getNearestMarker();
			if (nearestMarker != null)
				showInfoWindow(nearestMarker);

			this.moveToMyLocation = false;
		}
	}

	@Override
	public void onPause() {

		Log.d(LOG_TAG, "onPause");

		super.onPause();

		listenersOff();

		this.moveToMyLocation = true; // reset on leave
	}

	@Override
	public void onAttach(Activity activity) {

		Log.d(LOG_TAG, "onAttach");

		super.onAttach(activity);

		// This makes sure that the container activity has implemented
		// the callback interface. If not, it throws an exception
		try {
			this.updater = (Updater) activity;

			this.opener = (Opener) activity;
			this.opener.setOpenListener(this);

		} catch (ClassCastException e) {
			throw new ClassCastException(activity.toString()
					+ " must implement Updater");
		}
	}

	/**
	 * Checks if the map is ready (which depends on whether the Google Play
	 * services APK is available. This should be called prior to calling any
	 * methods on GoogleMap.
	 */
	private boolean checkReady() {
		if (this.map == null) {
			Log.w(LOG_TAG, "map is not ready");
			return false;
		}
		return true;
	}

	/**
	 * update UI with new location
	 * 
	 * @param myLocation
	 */
	private void newPosition(LatLng position) {
		Log.d(LOG_TAG, "--- new position: " + position);

		if (position == null) {
			Log.w(LOG_TAG, "null position is not allowed");
			return;
		}

		if (!checkReady())
			return;

		this.currentPosition = position;

		int markersSize = this.map.getMarkers().size(); // check first open

		Region oldRegion = this.updater.getRegion();
		Region newRegion = TabsActivity.findRegion(
				this.currentPosition.latitude, this.currentPosition.longitude);

		if (newRegion == null) {
			Log.w(LOG_TAG, "region for new position is not found");
			return;
		}

		if (newRegion.equals(oldRegion) && markersSize > 0) {
			Log.d(LOG_TAG, "change inside same region");
		} else {
			Log.d(LOG_TAG, "full region position update");

			this.updater.setRegion(newRegion);
			this.updater.read(false);
		}

	}

	private class NearCameraListener implements OnCameraChangeListener {

		@Override
		public void onCameraChange(CameraPosition position) {

			if (!checkReady())
				return;

			boolean withClustering = position.zoom < ZOOM_CLASTER_THRESHOLD;
			NearFragment.this.map
					.setClustering(buildClusteringSettings(withClustering));

			newPosition(position.target);
		}
	}

	private class NearMarkerClickListener implements OnMarkerClickListener {

		@Override
		public boolean onMarkerClick(Marker marker) {

			NearFragment.this.tracker.trackPageView("/markerClick");

			if (!checkReady())
				return true;

			boolean cluster = marker.isCluster();

			if (cluster) {
				List<Marker> markers = marker.getMarkers();
				Builder builder = LatLngBounds.builder();
				for (Marker m : markers) {
					builder.include(m.getPosition());
				}
				LatLngBounds bounds = builder.build();
				NearFragment.this.map.animateCamera(CameraUpdateFactory
						.newLatLngBounds(bounds, getResources()
								.getDimensionPixelSize(R.dimen.padding)));
			}

			return cluster; // consume event or not
		}
	}

	private class NearInfoWindowAdapter implements InfoWindowAdapter {

		private final LayoutInflater inflater;
		private final View mWindow;

		NearInfoWindowAdapter(Bundle savedInstanceState) {
			this.inflater = getLayoutInflater(savedInstanceState);

			this.mWindow = this.inflater.inflate(R.layout.custom_info_window,
					null);
		}

		@Override
		public View getInfoContents(Marker marker) {
			return null;
		}

		@Override
		public View getInfoWindow(Marker marker) {

			try {

				Integer id = Integer.valueOf(marker.getSnippet());

				List<BestRatePlace> list = filterById(NearFragment.this.places,
						id);

				Map<CurrencyCode, Map<OperationType, Double>> best = extractBest(NearFragment.this.places);

				if (list != null && !list.isEmpty()) {

					LinearLayout table = (LinearLayout) this.mWindow
							.findViewById(R.id.rates_table);

					BestRatePlace place = list.get(0);

					renderTextView(this.mWindow, R.id.title,
							place.getPlaceDescription());
					renderTextView(this.mWindow, R.id.work_hours,
							place.getWorkHours());

					// if have rates
					boolean hasValue = place.hasValue();

					table.removeAllViews(); // cleanup

					if (hasValue) {

						for (BestRatePlace item : list) {

							CurrencyCode currency = item.getCurrency();
							String title = getResources().getString(
									currency.getTitleRes());

							OperationType operaton = item.getOperationType();

							Double rateVal = item.getRateValue();
							Double bestVal = best.get(currency).get(operaton);

							Double diffVal = bestVal - rateVal;

							String value = TabsActivity.FMT_RATE_VALUE
									.format(rateVal);

							String diff = "";
							if (diffVal > -0.0001 && diffVal < 0.0001) {
								// zero diff
								diff = getResources().getString(
										R.string.lbl_best);
							} else {
								diff = TabsActivity.FMT_RATE_VALUE
										.format(diffVal);
							}

							View row = getRow(table, currency.getId());

							boolean buy = OperationType.BUY.equals(operaton);

							int valueId = buy ? R.id.curr_buy : R.id.curr_sell;
							int diffId = buy ? R.id.curr_buy_diff
									: R.id.curr_sell_diff;

							renderTextView(row, R.id.curr_title, title);
							renderTextView(row, valueId, value);
							renderTextView(row, diffId, diff);
						}
					}
				} else {
					Log.e(LOG_TAG, "can't find place with id: " + id);
				}

			} catch (Exception e) {
				Log.e(LOG_TAG, "can't show marker " + marker, e);
			}

			// Returning the view containing InfoWindow contents
			return this.mWindow;
		}

		private View getRow(LinearLayout table, int id) {

			// search existing
			int len = table.getChildCount();
			for (int i = 0; i < len; i++) {
				View child = table.getChildAt(i);

				if (child.getId() == id) {
					return child;
				}
			}

			// new row
			View row = this.inflater.inflate(R.layout.info_window_item, null);
			row.setId(id);
			table.addView(row);

			return row;
		}

		private void renderTextView(View parent, int textViewId,
				String textViewVal) {
			TextView titleUi = ((TextView) parent.findViewById(textViewId));
			titleUi.setText(textViewVal);
		}

	}

	/**
	 * set markers on map according provided places
	 * 
	 * @param places
	 */
	private synchronized void reloadMapItems(
			Collection<? extends BestRatePlace> places) {

		if (!checkReady())
			return;

		this.places.clear();
		this.places.addAll(places);

		this.map.clear();

		Set<Integer> added = new HashSet<Integer>();

		// Loop through all the items that are available to be placed on the
		// map
		for (BestRatePlace place : this.places) {

			int id = place.getId();

			if (added.contains(id)) {
				continue;
			}

			this.map.addMarker(new MarkerOptions() //
					.snippet(String.valueOf(id)) //
					.position(new LatLng(place.getX(), place.getY())) //
			);

			added.add(id);
		}
	}

	// TODO
	private Marker getNearestMarker() {
		Marker marker = null;

		if (this.currentPosition != null) {

			BestRatePlace nearestPlace = null;
			float nearestDistance = -1;

			for (BestRatePlace p : this.places) {

				double myLat = this.currentPosition.latitude;
				double myLng = this.currentPosition.longitude;
				float[] result = new float[1];
				double lat = p.getX();
				double lng = p.getY();

				Location.distanceBetween(myLat, myLng, lat, lng, result);

				if (nearestDistance == -1 || nearestDistance > result[0]) {
					nearestDistance = result[0];
					nearestPlace = p;
				}
			}

			Log.d(LOG_TAG, "nearest place: " + nearestPlace);

			List<Marker> markers = getRegularMarkers();
			marker = recursiveFind(String.valueOf(nearestPlace.getId()),
					markers);

		} else {
			Log.d(LOG_TAG,
					"since current position is undefined, nearest marker cant't be found.");
		}

		return marker;
	}

	/**
	 * find marker by snippet recursing by cluster
	 * 
	 * @param snippet
	 * @param markers
	 *            list to iterate
	 * 
	 * @return first marker with provided snippet or null if nothing found
	 */
	private Marker recursiveFind(String snippet, List<Marker> markers) {

		Marker res = null;

		for (Marker marker : markers) {
			if (marker.isCluster()) {
				res = recursiveFind(snippet, marker.getMarkers());
				break;
			} else {
				if (snippet.equals(marker.getSnippet())) {
					res = marker;
					break;
				}
			}
		}

		return res;
	}

	/**
	 * list visible and non-cluster markers
	 * 
	 * @return
	 */
	private List<Marker> getRegularMarkers() {
		List<Marker> markers = new ArrayList<Marker>();

		if (!checkReady())
			return markers;

		// This is the current user-viewable region of the map
		LatLngBounds bounds = this.map.getProjection().getVisibleRegion().latLngBounds;

		List<Marker> displayedMarkers = this.map.getDisplayedMarkers();
		for (Marker m : displayedMarkers) {
			boolean inBounds = bounds.contains(m.getPosition());
			if (inBounds && !m.isCluster() && m.isVisible()) {
				markers.add(m);
			}
		}

		return markers;
	}

	@Override
	public void onOpen(int id) {

		if (!checkReady())
			return;

		this.moveToMyLocation = false;

		this.map.setOnCameraChangeListener(null);

		Collection<? extends BestRatePlace> places = this.updater.getData();

		reloadMapItems(places);

		for (BestRatePlace p : places) {

			if (p.getId() == id) {

				List<Marker> markers = this.map.getMarkers();
				Marker m = recursiveFind(String.valueOf(p.getId()), markers);

				Log.d(LOG_TAG, "opening: " + id);

				showInfoWindow(m);

				break;
			}
		}

		this.map.setOnCameraChangeListener(this.cameraListener);
	}

	@Override
	public synchronized void onUpdate() {

		Log.d(LOG_TAG, "onUpdate");

		if (!checkReady())
			return;

		Collection<? extends BestRatePlace> places = this.updater.getData();

		reloadMapItems(places);
	}

	/**
	 * filter by place id
	 * 
	 * @param places
	 * @param id
	 * 
	 * @return
	 */
	private List<BestRatePlace> filterById(List<BestRatePlace> places, int id) {
		List<BestRatePlace> filtered = new ArrayList<BestRatePlace>();

		for (BestRatePlace place : places) {
			if (place.getId() == id) {
				filtered.add(place);
			}
		}

		Collections.sort(filtered, new Comparator<BestRatePlace>() {
			@Override
			public int compare(BestRatePlace lhs, BestRatePlace rhs) {
				return lhs.getCurrency().getId() - rhs.getCurrency().getId();
			}

		});

		return filtered;
	}

	/**
	 * get best values from list by currency and operation type
	 * 
	 * @param places
	 * @return
	 */
	private Map<CurrencyCode, Map<OperationType, Double>> extractBest(
			List<BestRatePlace> places) {

		Map<CurrencyCode, Map<OperationType, Double>> best = new LinkedHashMap<CurrencyCode, Map<OperationType, Double>>();

		for (BestRatePlace place : places) {

			OperationType operationType = place.getOperationType();
			CurrencyCode currency = place.getCurrency();
			Double rate = place.getRateValue();

			if (operationType == null || currency == null || rate == null) {
				continue;
			} // place without rates

			Map<OperationType, Double> currMap = best.get(currency);
			if (currMap == null) {
				currMap = new LinkedHashMap<OperationType, Double>();
				currMap.put(OperationType.BUY, null);
				currMap.put(OperationType.SELL, null);

				best.put(currency, currMap);
			}

			Double val = currMap.get(operationType);

			if (OperationType.BUY.equals(operationType)) {
				if (val == null || val < rate) {
					currMap.put(operationType, rate);
				}
			} else {
				if (val == null || val > rate) {
					currMap.put(operationType, rate);
				}
			}
		}

		return best;
	}

	@Override
	public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
		inflater.inflate(R.menu.near_menu, menu);
	}

	private void showInfoWindow(final Marker marker) {

		if (!checkReady())
			return;

		float zoomLevel = this.map.getMinZoomLevelNotClustered(marker);

		zoomLevel = zoomLevel < 1.0 ? ZOOM_CLASTER_THRESHOLD : zoomLevel; // too
																			// big

		float origZoom = this.map.getCameraPosition().zoom;

		zoomLevel = origZoom > zoomLevel ? origZoom : zoomLevel; // do not zoom
																	// out

		Log.d(LOG_TAG, "showing marker: " + marker + " at zoom:" + zoomLevel);

		this.map.moveCamera(CameraUpdateFactory.newLatLngZoom(
				marker.getPosition(), zoomLevel));

		marker.showInfoWindow();
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {

		Activity context = this.getActivity();

		if (context == null)
			return false;

		// item don't need map
		switch (item.getItemId()) {
		case android.R.id.home:
		case R.id.menu_best:
			Log.d(LOG_TAG, "open best rates list");

			this.updater.shiftFragments();

			return true;
		case R.id.menu_legal_notice:
			Log.d(LOG_TAG, "legal notice.");

			String LicenseInfo = GooglePlayServicesUtil
					.getOpenSourceSoftwareLicenseInfo(context);
			AlertDialog.Builder LicenseDialog = new AlertDialog.Builder(context);
			LicenseDialog.setTitle(R.string.btn_legal_notice);
			LicenseDialog.setMessage(LicenseInfo);
			LicenseDialog.show();

			return true;
		}

		// ------

		// map dependent items
		if (!checkReady())
			return false;

		// filter not cluster markers
		List<Marker> markers = getRegularMarkers();
		int size = markers.size();

		switch (item.getItemId()) {
		case R.id.menu_near_prev:
			Log.d(LOG_TAG, "prev marker");

			this.tracker.trackPageView("/nearPrev");

			if (size == 0) {
				return true;
			}

			for (int j = size - 1; j >= 0; j--) {
				Marker m = markers.get(j);

				if (m.isInfoWindowShown()) {
					m.hideInfoWindow();

					if (j > 0) {
						// show prev marker
						final Marker marker = markers.get(j - 1);
						showInfoWindow(marker);
						return true;
					}

					break;
				}
			}

			// if get there (no info windows shown or no prev marker) - show
			// last marker
			final Marker marker = markers.get(size - 1);
			showInfoWindow(marker);

			return true;
		case R.id.menu_near_next:
			Log.d(LOG_TAG, "next marker");

			this.tracker.trackPageView("/nearNext");

			if (size == 0) {
				return true;
			}

			for (int i = 0; i < size; i++) {
				Marker m = markers.get(i);

				if (m.isInfoWindowShown()) {
					m.hideInfoWindow();

					if (i < size - 1) {
						// show next marker
						final Marker nextMarker = markers.get(i + 1);
						showInfoWindow(nextMarker);

						return true;
					}

					break;
				}
			}

			// if get there (no info windows shown or no next marker) - show
			// first marker
			final Marker firstMarker = markers.get(0);
			showInfoWindow(firstMarker);

			return true;
		}

		return false; // should never happen
	}
}
